Generalized linear models: interactive
PLS 206 — Applied Multivariate Modeling
Apps for the GLM families (the Week 3 bonus deck: slides, PDF; the code). They run in the browser, so no R installation is needed; the first one takes a few seconds to load. Tick “Show R code” in any app to get code that reproduces the current view in RStudio.
Cheat sheet
Every GLM is three choices: a distribution for \(y\) (how it scatters around its mean), a linear predictor \(\eta = \beta_0 + \beta_1 x_1 + \dots\) (the same straight line as lm()), and a link \(g(\mu) = \eta\) that connects the mean of \(y\) to the line.
| \(y\) looks like | family | link \(g(\mu)\) | inverse link \(\mu = g^{-1}(\eta)\) | spread \(\text{Var}(y)\) | in R | \(e^{\beta_1}\) |
|---|---|---|---|---|---|---|
| any number, symmetric | Normal | \(\mu\) | \(\eta\) | \(\sigma^2\) | lm(y ~ x) |
(\(\beta_1\) adds to the mean) |
| 0 / 1 | Binomial | \(\log\frac{p}{1-p}\) | plogis(eta) |
\(p(1-p)\) | glm(y ~ x, family = binomial) |
odds ratio |
| \(k\) of \(n\) | Binomial | \(\log\frac{p}{1-p}\) | plogis(eta) |
\(n\,p(1-p)\) | glm(cbind(k, n - k) ~ x, family = binomial) |
odds ratio |
| counts | Poisson | \(\log\mu\) | exp(eta) |
\(\mu\) | glm(y ~ x, family = poisson) |
rate ratio |
| clumped counts | negative binomial | \(\log\mu\) | exp(eta) |
\(\mu + \mu^2/\theta\) | MASS::glm.nb(y ~ x) |
rate ratio |
| counts per effort | Poisson or NB + offset | \(\log\mu\) | effort * exp(eta) |
as above | glm(y ~ x + offset(log(effort)), family = poisson) |
rate ratio per unit effort |
| positive, right-skewed | Gamma | \(\log\mu\) | exp(eta) |
\(\mu^2/k\) | glm(y ~ x, family = Gamma(link = "log")) |
ratio of means |
Which family? Start from what kind of number \(y\) is. For counts, fit a Poisson and check deviance(m) / df.residual(m): well above 1 means switch to the negative binomial. If samples differ in effort (leaves scored, trap-days, quadrat area), add the offset.
The GLM family explorer
Pick a family. The left panel is the link scale, where every GLM is a straight line, \(\eta = \beta_0 + \beta_1 x\). The right panel is the data scale, where the inverse link bends that line into a mean the data can actually have. Move \(\beta_0\) and \(\beta_1\) and watch the line and the curve move together. Then tick “Also fit lm()” to see what a straight line on the raw data does instead.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 980
library(shiny)
fams <- c("Normal (identity link)" = "gaussian",
"Binomial, 0/1 (logit link)" = "bin01",
"Binomial, k of n (logit link)" = "bink",
"Poisson (log link)" = "poisson",
"Negative binomial (log link)" = "negbin",
"Poisson + offset (log link)" = "offset",
"Gamma (log link)" = "gamma")
defaults <- list(
gaussian = list(b0 = c(-5, 10, 2), b1 = c(-2, 2, 0.5)),
bin01 = list(b0 = c(-6, 6, -3), b1 = c(-1.5, 1.5, 0.6)),
bink = list(b0 = c(-6, 6, -3), b1 = c(-1.5, 1.5, 0.6)),
poisson = list(b0 = c(-2, 4, 0.5), b1 = c(-0.5, 0.5, 0.2)),
negbin = list(b0 = c(-2, 4, 0.5), b1 = c(-0.5, 0.5, 0.2)),
offset = list(b0 = c(-4, 2, -1.5), b1 = c(-0.5, 0.5, 0.1)),
gamma = list(b0 = c(-2, 5, 1), b1 = c(-0.5, 0.5, 0.2)))
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput("fam", "Family", fams, selected = "poisson"),
sliderInput("b0", "Intercept (β0)", -2, 4, 0.5, 0.1),
sliderInput("b1", "Slope (β1)", -0.5, 0.5, 0.2, 0.01),
sliderInput("n", "Observations (n)", 20, 500, 100, 10),
conditionalPanel("input.fam == 'gaussian'", sliderInput("sigma", "Spread (σ)", 0.2, 5, 1.5, 0.1)),
conditionalPanel("input.fam == 'bink'", sliderInput("trials", "Most trials per row (n_i up to)", 5, 200, 50, 5)),
conditionalPanel("input.fam == 'negbin'", sliderInput("theta", "Clumping θ (smaller = more spread)", 0.3, 50, 2, 0.1)),
conditionalPanel("input.fam == 'gamma'", sliderInput("shape", "Gamma shape k (bigger = less spread)", 0.5, 20, 2, 0.5)),
conditionalPanel("input.fam == 'offset'",
checkboxInput("effort_x", "Effort rises with x", TRUE),
checkboxInput("no_offset", "Also fit without the offset", TRUE)),
checkboxInput("show_lm", "Also fit lm() (straight line on the raw data)", FALSE),
actionButton("resample", "New sample"),
hr(),
checkboxInput("show_code", "Show R code", FALSE)
),
mainPanel(
plotOutput("plot", height = "430px"),
verbatimTextOutput("eq"),
verbatimTextOutput("stats"),
conditionalPanel("input.show_code == true", verbatimTextOutput("code_out"))
)
)
)
server <- function(input, output, session) {
seed <- reactiveVal(206)
observeEvent(input$resample, seed(seed() + 1), ignoreInit = TRUE)
observeEvent(input$fam, {
d <- defaults[[input$fam]]
updateSliderInput(session, "b0", min = d$b0[1], max = d$b0[2], value = d$b0[3])
updateSliderInput(session, "b1", min = d$b1[1], max = d$b1[2], value = d$b1[3])
}, ignoreInit = TRUE)
calc_code <- reactive({
f <- input$fam
head <- paste0('set.seed(', seed(), ')\nn <- ', input$n, '; b0 <- ', input$b0, '; b1 <- ', input$b1, '\n',
'x <- runif(n, 0, 10)\neta <- b0 + b1 * x # 2. the linear predictor\n')
body <- switch(f,
gaussian = paste0('mu <- eta # 3. identity link: the mean IS the line\n',
'y <- rnorm(n, mu, ', input$sigma, ') # 1. Normal: constant spread\n',
'dat <- data.frame(x, y)\n',
'model <- glm(y ~ x, family = gaussian, data = dat) # same as lm(y ~ x)\n'),
bin01 = paste0('mu <- plogis(eta) # 3. logit link: probability = plogis(line)\n',
'y <- rbinom(n, 1, mu) # 1. Bernoulli: each row is 0 or 1\n',
'dat <- data.frame(x, y)\n',
'model <- glm(y ~ x, family = binomial, data = dat)\n'),
bink = paste0('mu <- plogis(eta) # 3. logit link\n',
'trials <- sample(5:', input$trials, ', n, replace = TRUE) # n_i differs by row\n',
'y <- rbinom(n, trials, mu) # 1. Binomial: y successes of n_i\n',
'dat <- data.frame(x, y, trials)\n',
'model <- glm(cbind(y, trials - y) ~ x, family = binomial, data = dat)\n'),
poisson = paste0('mu <- exp(eta) # 3. log link: mean = exp(line)\n',
'y <- rpois(n, mu) # 1. Poisson: variance = mean\n',
'dat <- data.frame(x, y)\n',
'model <- glm(y ~ x, family = poisson, data = dat)\n'),
negbin = paste0('mu <- exp(eta) # 3. log link\n',
'y <- rnbinom(n, mu = mu, size = ', input$theta, ') # 1. NB: variance = mu + mu^2/theta\n',
'dat <- data.frame(x, y)\n',
'pois <- glm(y ~ x, family = poisson, data = dat) # for comparison\n',
'# negative binomial by maximum likelihood; glm.nb() in the MASS package gives the same estimates\n',
'nll <- function(p) -sum(dnbinom(y, mu = exp(p[1] + p[2] * x), size = exp(p[3]), log = TRUE))\n',
'fit <- optim(c(coef(pois), 0), nll, method = "BFGS", hessian = TRUE)\n',
'theta <- exp(fit$par[3])\n',
'est <- setNames(fit$par[1:2], c("(Intercept)", "x"))\n',
'se <- sqrt(diag(solve(fit$hessian)))[1:2]\n',
'tab <- cbind(Estimate = est, `Std. Error` = se, `z value` = est / se,\n',
' `Pr(>|z|)` = 2 * pnorm(-abs(est / se)))\n'),
offset = paste0(if (isTRUE(input$effort_x)) 'effort <- pmax(1, round(2 + 1.8 * x + runif(n, -2, 2))) # more effort at high x\n'
else 'effort <- round(runif(n, 1, 20)) # effort unrelated to x\n',
'rate <- exp(eta) # 3. log link: rate per unit effort\n',
'mu <- effort * rate # expected count = effort x rate\n',
'y <- rpois(n, mu) # 1. Poisson\n',
'dat <- data.frame(x, y, effort)\n',
'model <- glm(y ~ x + offset(log(effort)), family = poisson, data = dat)\n',
if (isTRUE(input$no_offset)) 'model_no <- glm(y ~ x, family = poisson, data = dat) # offset forgotten\n' else ''),
gamma = paste0('mu <- exp(eta) # 3. log link\n',
'y <- rgamma(n, shape = ', input$shape, ', rate = ', input$shape, ' / mu) # 1. Gamma: SD grows with mean\n',
'dat <- data.frame(x, y)\n',
'model <- glm(y ~ x, family = Gamma(link = "log"), data = dat)\n'))
tail <- paste0(if (f != "negbin") 'est <- coef(model)\ntab <- coef(summary(model))\n' else '',
'tab\n',
if (f != "gaussian") 'exp(est) # back on the data scale: ratios\n' else '',
if (isTRUE(input$show_lm)) paste0('ols <- lm(', if (f == "bink") 'y / trials' else if (f == "offset") 'y / effort' else 'y', ' ~ x, data = dat) # a straight line on the raw data\n') else '')
paste0(head, body, tail)
})
plot_code <- reactive({
f <- input$fam
yresp <- switch(f, bink = "dat$y / dat$trials", offset = "dat$y / dat$effort", "dat$y")
ylab_r <- switch(f, gaussian = "y", bin01 = "y (0/1) and P(y = 1)", bink = "y / trials", offset = "y / effort (rate)",
poisson = "count y", negbin = "count y", gamma = "y")
ylink <- switch(f,
gaussian = 'dat$y',
bin01 = 'NULL',
bink = 'qlogis((dat$y + 0.5) / (dat$trials + 1))',
offset = 'log((dat$y + 0.5) / dat$effort)',
gamma = 'log(dat$y)',
'log(dat$y + 0.5)')
ylab_l <- switch(f, gaussian = "eta = mean of y", bin01 = "eta = log-odds", bink = "eta = log-odds",
offset = "eta = log(rate)", gamma = "eta = log(mean)", "eta = log(mean)")
linkinv <- switch(f, gaussian = "identity", bin01 = "plogis", bink = "plogis", "exp")
paste0(
'grid <- data.frame(x = seq(0, 10, length.out = 200), effort = 1)
fit_eta <- est[1] + est[2] * grid$x # the fitted line
true_eta <- b0 + b1 * grid$x
par(mfrow = c(1, 2))
# left: the link scale, where the model is a straight line
yl <- ', ylink, '
', if (f == "bin01") '# 0/1 data have no logit of their own: bin x and take the log-odds of each bin
bins <- cut(dat$x, seq(0, 10, 1), include.lowest = TRUE)
k <- tapply(dat$y, bins, sum); m <- tapply(dat$y, bins, length)
bx <- tapply(dat$x, bins, mean); yl <- log((k + 0.5) / (m - k + 0.5))
' else '', 'plot(', if (f == "bin01") 'bx' else 'dat$x', ', yl, pch = 16, col = adjustcolor("steelblue", 0.5),
xlim = c(0, 10), ylim = range(yl, true_eta, fit_eta, finite = TRUE),
xlab = "x", ylab = "', ylab_l, '", main = "link scale: a straight line")
lines(grid$x, true_eta, col = "#00A060", lwd = 3)
lines(grid$x, fit_eta, lty = 2, lwd = 2)
# right: the data scale, where the inverse link bends the line
yr <- ', if (f == "bin01") 'jitter(dat$y, amount = 0.03)' else yresp, '
plot(dat$x, yr, pch = 16, col = adjustcolor("steelblue", 0.5),
ylim = range(yr, ', linkinv, '(true_eta)', if (isTRUE(input$show_lm)) ', predict(ols, grid)' else '', '),
xlab = "x", ylab = "', ylab_r, '", main = "data scale: ', linkinv, '(line)")
lines(grid$x, ', linkinv, '(true_eta), col = "#E0524A", lwd = 3)
lines(grid$x, ', linkinv, '(fit_eta), lty = 2, lwd = 2)
', if (f == "offset" && isTRUE(input$no_offset)) 'lines(grid$x, predict(model_no, grid, type = "response") / mean(dat$effort), col = "purple", lwd = 2, lty = 3)
' else '',
if (isTRUE(input$show_lm)) 'abline(ols, col = "grey30", lwd = 2, lty = 4)
' else '',
'legend("topleft", bty = "n", cex = 0.85,
legend = c("truth", "glm fit"', if (f == "offset" && isTRUE(input$no_offset)) ', "no offset (per average effort)"' else '',
if (isTRUE(input$show_lm)) ', "lm() fit"' else '', '),
col = c("#E0524A", "black"', if (f == "offset" && isTRUE(input$no_offset)) ', "purple"' else '',
if (isTRUE(input$show_lm)) ', "grey30"' else '', '), lty = c(1, 2', if (f == "offset" && isTRUE(input$no_offset)) ', 3' else '',
if (isTRUE(input$show_lm)) ', 4' else '', '), lwd = 2)
')
})
res <- reactive({
env <- new.env()
eval(parse(text = calc_code()), envir = env)
env
})
output$plot <- renderPlot({
env <- res()
par(mar = c(4.5, 4.5, 2.5, 1))
eval(parse(text = plot_code()), envir = env)
})
output$eq <- renderText({
f <- input$fam
lp <- sprintf("%.2f + %.2f x", input$b0, input$b1)
switch(f,
gaussian = sprintf("1. distribution: y ~ Normal(mu, sigma = %s)\n2. line: eta = %s\n3. link: mu = eta (identity)\n spread: Var(y) = sigma^2, the same everywhere", input$sigma, lp),
bin01 = sprintf("1. distribution: y ~ Bernoulli(p)\n2. line: eta = %s\n3. link: log(p / (1 - p)) = eta, so p = plogis(eta)\n spread: Var(y) = p(1 - p)", lp),
bink = sprintf("1. distribution: y ~ Binomial(n_i, p)\n2. line: eta = %s\n3. link: log(p / (1 - p)) = eta, so p = plogis(eta)\n spread: Var(y) = n_i p(1 - p)", lp),
poisson = sprintf("1. distribution: y ~ Poisson(mu)\n2. line: eta = %s\n3. link: log(mu) = eta, so mu = exp(eta)\n spread: Var(y) = mu", lp),
negbin = sprintf("1. distribution: y ~ NegBin(mu, theta = %s)\n2. line: eta = %s\n3. link: log(mu) = eta, so mu = exp(eta)\n spread: Var(y) = mu + mu^2 / theta", input$theta, lp),
offset = sprintf("1. distribution: y ~ Poisson(mu)\n2. line: eta = %s\n3. link: log(mu) = log(effort) + eta, so mu / effort = exp(eta)\n spread: Var(y) = mu", lp),
gamma = sprintf("1. distribution: y ~ Gamma(mean mu, shape k = %s)\n2. line: eta = %s\n3. link: log(mu) = eta, so mu = exp(eta)\n spread: Var(y) = mu^2 / k (SD grows with the mean)", input$shape, lp))
})
output$stats <- renderPrint({
e <- res(); f <- input$fam
cf <- e$tab
cat("Fitted model (truth: b0 =", input$b0, " b1 =", input$b1, ")\n")
print(signif(cf, 3))
b1 <- e$est[2]
cat("\nWhat beta1 means: one unit more x ...\n")
cat(switch(f,
gaussian = sprintf(" adds %.3f to the mean of y\n", b1),
bin01 = , bink = sprintf(" multiplies the odds by exp(%.3f) = %.3f (odds ratio)\n", b1, exp(b1)),
offset = sprintf(" multiplies the rate per unit effort by exp(%.3f) = %.3f (rate ratio)\n", b1, exp(b1)),
sprintf(" multiplies the mean by exp(%.3f) = %.3f\n", b1, exp(b1))))
if (f %in% c("poisson", "negbin", "offset")) {
pm <- if (f == "negbin") e$pois else e$model
cat(sprintf("\nPoisson check: deviance / df = %.2f (near 1 if Poisson fits)\n", deviance(pm) / df.residual(pm)))
}
if (f == "negbin") cat(sprintf("Estimated theta = %.2f\nSE of the slope: Poisson %.4f, negative binomial %.4f\n",
e$theta, coef(summary(e$pois))[2, 2], cf[2, 2]))
if (f == "offset" && isTRUE(input$no_offset))
cat(sprintf("exp(b1) without the offset: %.3f\nexp(b1) with the offset: %.3f\ntruth: %.3f\n",
exp(coef(e$model_no)[2]), exp(b1), exp(input$b1)))
if (isTRUE(input$show_lm)) cat(sprintf("\nlm() slope on the raw data: %.4f\n(a straight line, no link, constant spread)\n", coef(e$ols)[2]))
})
output$code_out <- renderText(paste0(calc_code(), "\n", plot_code()))
}
shinyApp(ui, server)
The family is a choice about spread
Two families can share the same mean and the same link and still disagree about the data. Set a mean and compare what each family says one observation could look like. Poisson ties the variance to the mean; the negative binomial adds clumping on top; the Gamma keeps the coefficient of variation fixed; the Normal ignores the mean entirely.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 760
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("mu", "The mean (μ)", 0.5, 50, 5, 0.5),
sliderInput("sigma", "Normal: σ", 0.5, 20, 3, 0.5),
sliderInput("theta", "Negative binomial: θ", 0.3, 50, 2, 0.1),
sliderInput("shape", "Gamma: shape k", 0.5, 20, 2, 0.5),
hr(),
checkboxInput("show_code", "Show R code", FALSE),
helpText("Raise the mean: the Normal keeps the same width, the Poisson widens slowly (SD = √μ), the negative binomial and Gamma widen in proportion to μ.")
),
mainPanel(
plotOutput("plot", height = "460px"),
verbatimTextOutput("stats"),
conditionalPanel("input.show_code == true", verbatimTextOutput("code_out"))
)
)
)
server <- function(input, output, session) {
code <- reactive(paste0(
'mu <- ', input$mu, '
counts <- 0:max(20, ceiling(mu * 4))
x <- seq(0.01, max(20, mu * 4), length.out = 400)
par(mfrow = c(2, 2))
plot(x, dnorm(x, mu, ', input$sigma, '), type = "l", lwd = 2, col = "#0070C0",
main = "Normal: Var = sigma^2", xlab = "y", ylab = "density"); abline(v = mu, lty = 2)
plot(counts, dpois(counts, mu), type = "h", lwd = 3, col = "#00A060",
main = "Poisson: Var = mu", xlab = "y", ylab = "probability"); abline(v = mu, lty = 2)
plot(counts, dnbinom(counts, mu = mu, size = ', input$theta, '), type = "h", lwd = 3, col = "#00A060",
main = "Negative binomial: Var = mu + mu^2/theta", xlab = "y", ylab = "probability"); abline(v = mu, lty = 2)
plot(x, dgamma(x, shape = ', input$shape, ', rate = ', input$shape, ' / mu), type = "l", lwd = 2, col = "#7030A0",
main = "Gamma: Var = mu^2/k", xlab = "y", ylab = "density"); abline(v = mu, lty = 2)
'))
output$plot <- renderPlot({ par(mar = c(4.2, 4.2, 2.5, 1)); eval(parse(text = code())) })
output$stats <- renderPrint({
mu <- input$mu
sds <- c(Normal = input$sigma, Poisson = sqrt(mu), `Negative binomial` = sqrt(mu + mu^2 / input$theta),
Gamma = mu / sqrt(input$shape))
cat(sprintf("Mean = %.1f in every panel (dashed line). Standard deviation of one observation:\n", mu))
print(data.frame(SD = round(sds, 2)))
cat(sprintf("\nP(y = 0): Poisson %.3f negative binomial %.3f\n", dpois(0, mu), dnbinom(0, mu = mu, size = input$theta)))
})
output$code_out <- renderText(code())
}
shinyApp(ui, server)
Why the offset
Lesions counted on the leaves scored, insects per trap-day, seeds per quadrat: the count depends on how hard you looked. The offset, + offset(log(effort)), models the count per unit effort. Set how strongly effort is tied to x and compare the two fits. When effort is unrelated to x, leaving the offset out only adds noise. When effort rises with x, it biases the slope.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 820
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("b1", "True effect of x on the rate (β1, log scale)", -0.3, 0.3, 0.05, 0.01),
sliderInput("tie", "How strongly effort rises with x", 0, 1, 0.8, 0.05),
sliderInput("n", "Samples (n)", 20, 400, 120, 10),
actionButton("resample", "New sample"),
hr(),
checkboxInput("show_code", "Show R code", FALSE),
helpText("Set β1 = 0 and the tie to 1: nothing happens to the rate, but the model without the offset finds a strong effect of x.")
),
mainPanel(
plotOutput("plot", height = "400px"),
verbatimTextOutput("stats"),
conditionalPanel("input.show_code == true", verbatimTextOutput("code_out"))
)
)
)
server <- function(input, output, session) {
seed <- reactiveVal(206)
observeEvent(input$resample, seed(seed() + 1), ignoreInit = TRUE)
code <- reactive(paste0(
'set.seed(', seed(), ')
n <- ', input$n, '; b1 <- ', input$b1, '; tie <- ', input$tie, '
x <- runif(n, 0, 10)
effort <- round(exp(log(10) + tie * (x - 5) * 0.3 + rnorm(n, 0, 0.4))) # e.g. leaves scored
effort <- pmax(effort, 1)
rate <- exp(-1 + b1 * x) # true count per unit effort
y <- rpois(n, effort * rate)
with_off <- glm(y ~ x + offset(log(effort)), family = poisson)
no_off <- glm(y ~ x, family = poisson)
coef(summary(with_off))["x", ]
coef(summary(no_off))["x", ]
par(mfrow = c(1, 3))
plot(x, effort, pch = 16, col = "grey40", main = "effort vs x")
plot(x, y, pch = 16, col = adjustcolor("steelblue", 0.6), cex = sqrt(effort / 5), main = "raw counts (size = effort)")
plot(x, y / effort, pch = 16, col = adjustcolor("steelblue", 0.6), main = "count per unit effort", ylab = "y / effort")
g <- data.frame(x = seq(0, 10, 0.1), effort = 1)
lines(g$x, exp(-1 + b1 * g$x), col = "#E0524A", lwd = 3) # truth
lines(g$x, predict(with_off, g, type = "response"), lty = 2, lwd = 2) # with offset
lines(g$x, predict(no_off, g, type = "response") / mean(effort), col = "purple", lty = 3, lwd = 2) # without
legend("topleft", bty = "n", c("truth", "with offset", "no offset"), col = c("#E0524A", "black", "purple"), lty = 1:3, lwd = 2)
'))
parts <- reactive(strsplit(code(), "\npar(mfrow", fixed = TRUE)[[1]])
res <- reactive({ e <- new.env(); eval(parse(text = parts()[1]), envir = e); e })
output$plot <- renderPlot({
e <- res(); par(mar = c(4.2, 4.2, 2.5, 1))
eval(parse(text = paste0("par(mfrow", parts()[2])), envir = e)
})
output$stats <- renderPrint({
e <- res()
a <- coef(summary(e$with_off))["x", ]; b <- coef(summary(e$no_off))["x", ]
cat(sprintf("Correlation of effort with x: %.2f\n\n", cor(e$x, e$effort)))
cat(sprintf("Truth: exp(b1) = %.3f per unit x\n", exp(input$b1)))
cat(sprintf("With offset: exp(b1) = %.3f p = %.2g\n", exp(a[1]), a[4]))
cat(sprintf("No offset: exp(b1) = %.3f p = %.2g\n", exp(b[1]), b[4]))
})
output$code_out <- renderText(code())
}
shinyApp(ui, server)